Video Thumbnail
3:38
3:32
clock icon Created with Sketch. 3 minutes

Solution: Inheritance


Alberto Miño

Hi team,

My solution:

************ Start Code ************

from typing import Any
from abc import ABC, abstractmethod
import requests
from dataclasses import dataclass, field

class CityNotFoundError(Exception):
pass

class InsuficientParams(Exception):
pass

class ApiGetter(ABC):
@abstractmethod
def fetch(self, url: str) -> dict[str, Any]:
"""Method that implements request library"""

@dataclass
class RequestApiGetter(ApiGetter):
timeout_value: int = 5

@property
def timeout(self) -> int:
return self.timeout_value

@timeout.setter
def timeout(self, value: int) -> None:
if isinstance(value, int):
self.timeout_value = value
else:
self.timeout_value = int(value)

def fetch(self, url: str) -> dict[str, Any]:
print(url)
return requests.get(url, timeout=self.timeout_value).json()

class ApiWeatherService(ABC):
"Implements a way to get the data "

@abstractmethod
def retrieve_forecast(self, city: str) -> None:
"""Retrieve data from a wheater API service"""

@dataclass
class WeatherService(ApiWeatherService):
api_key: str
api_getter: ApiGetter
full_weather_forecast: dict[str, Any] = field(init=False)
url = f"http://api.openweathermap.org/data/2.5/weather"

def __post_init__(self) -> None:
self.url += f"?appid={self.api_key}"

def retrieve_forecast(self, city:str) -> None:
if not city:
raise InsuficientParams("You should submit at least city and keys as params")
response = self.api_getter.fetch(f"{self.url}&q={city}")
if "main" not in response:
raise CityNotFoundError(
f"Couldn't find weather data. Check '{city}' if it exists and is correctly spelled.\n"
)

self.full_weather_forecast = response

@dataclass
class MyWeatherService:
weather_service: WeatherService

def retrieve_forecast(self, city: str) -> None:
self.weather_service.retrieve_forecast(city)
# print the temperature in Celsius
temp = self.weather_service.full_weather_forecast["main"]["temp"] - 273.15
print(f"The current temperature in {city} is {temp:.1f} °C.")

if __name__ == "__main__":
api_key="1234567"
city="Mar del Plata"
api_getter = RequestApiGetter()
service = WeatherService(api_key, api_getter)
MyWeatherService(service).retrieve_forecast(city)

************ End Code ************
I could have separate the print statement to separate responsibilities but just tried to focus on remove inheritance.

REPLY
Andreas [ArjanCodes Team]

Hi Alberto! Thanks for your submission!

This solution looks good! It does not rely on inheritance and uses composition. However, some minor remarks can be improved.

First and foremost, the api_key looks like an active key. Be sure to remove it!!

Let's continue with the minor remarks:

* Usually, you would not use the if __name__ == "__main__": statement to execute logic. Instead, define a main function that is called with the if __name__ == "__main__": block
* Nice that you are using custom exceptions!
* Currently, this solution relies on side effects, the self.weather_service.retrieve_forecast(city) needs to set a state for temp = self.weather_service.full_weather_forecast["main"]["temp"] - 273.15 to work properly. I would recommend that self.weather_service.retrieve_forecast(city) instead returns a value which is later used in the retrieve_forecast method

Other than that, this solution looks good!

REPLY
Alberto Miño

Hi Andreas, how are you?

I didn't realice about the api_key :D

Thanks for the remarks!

REPLY
Andreas [ArjanCodes Team]

Hi! I am doing well, and I hope you are as well!

No worries, as long as you revoked it from the service, it should be fine

REPLY
Philipp Walter

Hi Arjan,

since you motivated me to think about functional approaches more often i created the following.
Idea, the printing could also be used by a home_automation calling sensors by names and returning the data.
(To be honest, the variable and function naming could be better ;-))

from functools import partial
import os
from typing import Callable
from dotenv import load_dotenv
import requests
from pydantic import BaseModel

class CityNotFoundError(Exception):
pass

class BasicWeatherInfo(BaseModel):
temp: float
feels_like: float
pressure: int
humidity: int

def weather_service(city: str, api_key: str) -> BasicWeatherInfo:
url = f"http://api.openweathermap.org/data/2.5/weather?q={city}&appid={api_key}"
response = requests.get(url, timeout=5).json()
if "main" not in response:
raise CityNotFoundError(
f"Couldn't find weather data. Check '{city}' if it exists and is correctly spelled.\n"
)
return BasicWeatherInfo(**response["main"])

def create_personal_weather_service() -> Callable[[str], BasicWeatherInfo]:
load_dotenv()
api_key = os.getenv("API_KEY")
if api_key is None:
raise ValueError("API_KEY is not set")
return partial(weather_service, api_key=api_key)

def print_weather(city: str, weather_service: Callable[[str], BasicWeatherInfo]) -> None:
weather = weather_service(city)
print(f"The current temperature in {city} is {weather.temp - 273.15:.1f} °C.")
print(f"It feels like {weather.feels_like - 273.15:.1f} °C.")
print(f"The pressure is {weather.pressure} hPa.")
print(f"The humidity is {weather.humidity}%.")

if __name__ == "__main__":

personal_weather_service = create_personal_weather_service()
for city in ["Utrecht, NL", "Amsterdam, NL", "Rotterdam, NL"]:
print_weather(city, personal_weather_service)

REPLY
Andreas [ArjanCodes Team]

hahaha, well naming is one (if not the) hardest thing to do in software, hopefully, it gets better with experience :( Nice to hear that you aim to use functions as a start, in my experience, it tends to lead to more understandable and testable code!

Just one remark, it is not conventional to write logic underneath if __name__ == "__main__": . Conventionally, we want a main function that contains all the logic, then that we call the main function under if __name__ == "__main__":

REPLY
Sylvain Payot

hi Arjan,
-- apologies if that question was already addressed in the chat below --
it feels a bit awkward for the client to have a temperature property, doesn't it?
wouldn't it make sense to keep the client as a client, whose job is simply to make it easy to query the API and instead capture the output of retrieve_forecast in a different data structure (let's call it WeatherForecast for the sake of the argument :)). WeatherForecast can then own the temperature property you introduced in your solution, and whatever over response parsing logic we may want to add later on...

REPLY
Andreas [ArjanCodes Team]

Hi! It is more of a domain question. What you are proposing is a good solution! The challenge here was mostly to remove the abusive inheritance, but separating the client and data storage is a very good approach.

I will add that as an feedback to the course and hopefully in the future that is something that we can update!

REPLY
Fulton Q Shannon

I added two more properties. I also added a prompt for the user to enter a city. I am in the United States, so I added New York, Washington DC, Los Angeles, etc.....

@property
def humidity(self) -> float:
try:
return self.full_weather_forecast["main"]["humidity"]
except KeyError:
raise ValueError("Humidity data is not available in the forecast.")

@property
def wind_speed(self) -> float:
try:
return self.full_weather_forecast["wind"]["speed"]
except KeyError:
raise ValueError("Wind speed data is not available in the forecast.")

Then I called them in main()
# Prompt user for city input
city = input("Enter the name of the city for the weather report: ")
client = WeatherService(api_key=API_KEY)
try:
client.retrieve_forecast(city)
print(f"The current temperature in {city} is {client.temperature:.1f} °C.")
print(f"Humidity in {city} is {client.humidity}%.")
print(f"Wind speed in {city} is {client.wind_speed} meters/sec.")
except CityNotFoundError as e:
print(e)
except requests.exceptions.RequestException as e:
print(f"Request failed: {e}")
except Exception as e:
print(f"An error occurred: {e}")

REPLY
Andreas [ArjanCodes Team]

Nice solution!

REPLY
Stanley Sims

--What I did--
from dataclasses import dataclass
from typing import Any, Protocol
import requests

KELVIN_TO_CELSIUS_CONV = 273.15
OWM_API_KEY = "blah blah blah"
OWM_BASE_URL = "http://api.openweathermap.org/data/2.5/weather"

class CityNotFoundError(Exception):
pass

def convert_kelvin_to_celsius(temp: float) -> float:
""" Convert temperature from Kelvin to Celsius """
return temp - KELVIN_TO_CELSIUS_CONV

class WeatherForecastData(Protocol):
""" Only temps for now but could have other data points (humidity, wind direction) """
@property
def temp(self) -> float:
...

class WeatherService(Protocol):
def retrieve_forecast(self, city: str) -> WeatherForecastData:
""" Return city's forecast data """
...

def retrieve_forecast(city: str, ws: WeatherService) -> WeatherForecastData:
""" Retrieve forecast data for city with any defined weather api service """
return ws.retrieve_forecast(city)

@dataclass
class OpenWeatherMapForecastData(WeatherForecastData):
base_data: dict[str, Any]

@property
def temp(self) -> float:
if "temp" not in self.base_data["main"].keys():
raise KeyError(f"Invalid key 'temp' to retrieve temperature...check api reference")
return convert_kelvin_to_celsius(self.base_data["main"]["temp"])

@dataclass
class OpenWeatherMapApi(WeatherService):
base_url: str
api_key: str

def retrieve_forecast(self, city: str) -> OpenWeatherMapForecastData:
url = f"{self.base_url}?q={city}&appid={self.api_key}"
response = requests.get(url, timeout=5).json()
if "main" not in response:
raise CityNotFoundError(
f"Couldn't find weather data. Check '{city}' if it exists and is correctly spelled.\n"
)

return OpenWeatherMapForecastData(response)

if __name__ == "__main__":
ow_service = OpenWeatherMapApi(
base_url=OWM_BASE_URL,
api_key=OWM_API_KEY,
)
city = "Utrecht"
wfd = retrieve_forecast(city, ow_service)
print(f"The current temperature in {city} is {wfd.temp:.1f} °C.")

Hmm...could've made the api call non-blocking...

Yeah, I'll go back and put at least the api key in a config file. In the videos, I hear .env files mentioned often as a way to keep things out of source control. Is .env preferable to .toml? If so, what are the advantages?

REPLY
Stanley Sims

Lol, if the use of Protocols counts as inheritance, I'll have to do this one again. I'll go ahead and roll another version w/o using the inheritance syntax...I need the practice!

REPLY
Arjan Egges

Hi Stanley, I personally have a preference for .env files, because they are widely supported, especially when you deal with CI/CD pipelines and containers hosted in the cloud. But I must also admit that I haven't looked at using TOML files for variables in a while, so perhaps support for them has improved.

REPLY
Show More